package playGround;


import java.lang.reflect.Array;
import java.math.BigDecimal;


import be.kuleuven.cs.som.annotate.*;

/**
 * @invar 	The temperature of each square must be a valid temperature.
 * 			| isValidTemperature(temperature)
 * @invar   The temperatureScale must be a valid scale defined by enumtype.
 * 			| canHaveTemperatureScale(temperatureScale)
 * @invar   The minTemperature must be a valid minTemperature.
 * 			| canHaveMinTemperature(minTemperature)
 * @invar   The maxTemperature must be a valid maxTemperature.
 * 			| canHaveMaxTemperature(maxTemperature)
 * @author Maarten
 *
 */
public class Square {
	/**
	 * 
	 * @param 	temperature
	 * 			The temperature for this new square.
	 * @param 	humidity
	 * 			The humidity for this new square.
	 * @param   heatConstant
	 * 			The heatconstant of this square.
	 * @pre     The humidity must lie between 0 and 100 percent.
	 * 			|canHaveHumidity(humidity)
	 * @post 	The humidity of this new square is set to the given humidity.
	 * 			|new.getHumidity() == humidity
	 * @post	The temperature of this new square is set to the given temperature.
	 * 			|new.getTemperature() == temperature
	 * @post    The temperatureScale of the square is set to the indicated temperatureScale.
	 * 			|new.getTemperatureScale() == temperatureScale	
	 * @throws 	IllegalTemperatureException
	 * 			The given temperature is not a valid temperature for this square.
	 * 			|!isValidTemperature(temperature)
	 * @throws IllegalArgumentException
	 * 			The given temperatureScale is not valid.
	 * 			|!canHaveTemperatureScale(temperatureScale)
	 */
	public Square(int temperature, BigDecimal humidity, Scale temperatureScale, float heatConstant)
			throws IllegalTemperatureException, IllegalArgumentException{
		if (!isValidTemperature(temperature))
			throw new IllegalTemperatureException(temperature);
		setTemperature(temperature);
		if(!canHaveTemperatureScale(temperatureScale)){
			throw new IllegalArgumentException("not a valid scale");
		}
		if(!canHaveHeatConstant(heatConstant)){
			throw new IllegalArgumentException("not a valid constant");
		}
		this.temperatureScale = temperatureScale;
		
		setHumidity(humidity);
		calculateColdDamage();
	    calculateHeatDamage();
		calculateRustDamage();
		calculateInhability();
		calculateSlippery();
		heatConstant = Math.round(heatConstant*10)/10;
	}
	/**
	 * @post	The temperature of this new square is set to the default value 0.
	 * 			|new.getTemperature()==0
	 * @post	The humidity of this new square is set to the default value 0.
	 * 			|new.getHumidity()==0
	 * @post	The scale of this new square is set to Celcius.
	 * 			|new.getScale()== Scale.CELCIUS
	 * @post	The heatconstant of this new square is set to 0.1f.
	 * 			|new.getHeatConstant == 0.1f
	 */
	public Square() {
		this(0,new BigDecimal(0), Scale.CELCIUS,0.1f);
	}
	/**
	 * The humidity of this square.
	 */
	private BigDecimal humidity;
	/**
	 * The temperatureScale of this square.
	 */
	private Scale temperatureScale;
	/**
	 * The minimum temperature possible in this square.
	 */
	private int minTemperature=-200;
	/**
	 * The maximum temperature possible in this square.
	 */
	private int maxTemperature=5000;
	/**
	 * The temperature of this square.
	 */
	private int temperature;
	/**
	 * The cold damage received while standing on this square.
	 */
	private int coldDamage;
	/**
	 * the heat damage received while standing on this square.
	 */
	private int heatDamage;
	/**
	 * The rust damage received while standing on this square.
	 */
	private int rustDamage;
	/**
	 * The rate of inhability of this square.
	 */
	private int inhability;
	/**
	 * 
	 */
	private static int heatLimit;
	private static int heatRate;
	/**
	 * Variable registering the slipperiness of this square.
	 */
	private boolean slippery;
	/**
	 * Variable registering the sliperiness of the material this square is made of.
	 */
	private boolean slipperyMaterial;
	/**
	 * Array that keeps record of the borders of this square with other squares.
	 */
	private boolean[] borders= {true,true,true,true,true,true};
	
	private static float heatConstant;
	
	/**
	 * 
	 * @param temperatureScale is the scale for the temperature at the moment.
	 * @return returns true if the temperatureScale is a valid one.
	 * 			| temperatureScale == Scale.Celcius || temperatureScale == Scale.Fahrenheit
	 */
	public boolean canHaveTemperatureScale(Scale temperatureScale){
		if(temperatureScale==null)
			return false;
		else 
			return temperatureScale == Scale.CELCIUS || temperatureScale == Scale.FAHRENHEIT || temperatureScale == Scale.KELVIN;
	}
	/**
	 * 
	 * @return the temperatureScale of this square.
	 */
	@Basic
	public Scale getTemperatureScale() {
		return this.temperatureScale;
	}
	
	/**
	 * 
	 * @return returns the minTemperature of this square
	 */
	@Basic @Raw
	public int getMinTemperature(){
		return this.minTemperature;
	}
	/**
	 * 
	 * @param minTemperature is the minTemperature that is given to the method.
	 * @return returns if the minTemperature is a valid one: it must be higher than or equal to the lowest integer value and lower than or equal to the maxTemperature.
	 * 			| result== ((minTemperature >= Integer.MIN_VALUE) && (minTemperature <= getMaxTemperature())
	 */
	public boolean canHaveMinTemperature(int minTemperature){
		
		return minTemperature >= Integer.MIN_VALUE && minTemperature <= getMaxTemperature();
	}
	/**
	 * 
	 * @param temperature is the temperature given to the method
	 * @throws IllegalArgumentException: if the temperature is not a valid one.
	 * 			| ! canHaveMinTemperature(temperature)
	 * @post  The new minTemperature of this square is set to temperature.
	 * 			| new.minTemperature() = temperature
	 */
	public void setMinTemperature(int temperature)throws IllegalArgumentException{
		if(!canHaveMinTemperature(temperature))
			throw new IllegalArgumentException();
		else{
			this.minTemperature = temperature;
		}
	}
	
	/**
	 * 
	 * @return the maxTemperature from this square
	 */
	@Basic @Raw
	public int getMaxTemperature(){
		return this.maxTemperature;
	}
	
	/**
	 * 
	 * @param maxTemperature is the maxTemperature that is given to the method.
	 * @return returns if the maxTemperature is a valid one: it must be lower than or equal to the lowest integer value and higher than or equal to the minTemperature.
	 * 			| result== ((maxTemperature <= Integer.MAX_VALUE) && (maxTemperature >= getMinTemperature()))
	 */
	public boolean canHaveMaxTemperature(int maxTemperature){
		
		return maxTemperature <= Integer.MAX_VALUE && maxTemperature >= getMinTemperature();
	}
	
	/**
	 * 
	 * @param temperature is the temperature given to the method
	 * @throws IllegalArgumentException  if the temperature is not a valid one.
	 * 			| ! canHaveMaxTemperature(temperature)
	 * @post  The new maxTemperature of this square is set to temperature.
	 * 			| new.maxTemperature() = temperature
	 */
	public void setMaxTemperature(int temperature)throws IllegalArgumentException{
		if(canHaveMaxTemperature(temperature))
			this.maxTemperature = temperature;
		else{
			throw new IllegalArgumentException();
		}
	}
	
	/**
	 * @return the temperature of this square
	 */
	@Basic @Raw
	public int getTemperature() {
		return this.temperature;
	}
	/**
	 * @param temperature the temperature to set
	 * @post	The new temperature is set to the given temperature.
	 * 			|new.getTemperature() == temperature	
	 * @throws 	IllegalTemperatureException
	 * 			The given temperature is not a valid temperature for this square.
	 * 			|!isValidTemperature(temperature)
	 */
	public void setTemperature(int temperature)
			throws IllegalTemperatureException{
		if (!isValidTemperature(temperature))
			throw new IllegalTemperatureException(temperature);
		this.temperature=temperature;
		
	}
	/**
	 * 
	 * @param 	temperature check if it is a correct temperature
	 * @return 	True if and only if the given temperature lies between the values: minTemperature and maxTemperature.
	 * 			| result==(temperature>=getMinTemperature() && temperature <=getMaxTemperature())
	 */
	public boolean isValidTemperature(int temperature) {
		return (temperature>=getMinTemperature() && temperature <=getMaxTemperature());
	}
	/**
	 * 
	 * @param newTemperatureScale you want to turn the scale in to.
	 * @throws NullPointerException if the given newTemperatureScale is null
	 * 			| newTemperatureScale == null
	 * @post the new temperature scale is the given newTemperatureScale.
	 * 			|new.getTemperatureScale() == newTemperatureScale
	 * @post the new minTemperature is adjusted to the new scale.
	 * 			|new.getMinTemperature() == Scale.changeScale(getTemperatureScale(),newTemperatureScale,this.getMinTemperature())
	 * @post the new maxTemperature is adjusted to the new scale.
	 * 			|new.getMaxTemperature() == Scale.changeScale(getTemperatureScale(),newTemperatureScale,this.getMaxTemperature())
	 * @post the new temperature is adjusted to the new scale.
	 * 			|new.getTemperature() == Scale.changeScale(getTemperatureScale(),newTemperatureScale,this.getTemperature())
	 */
	public void changeScale(Scale newTemperatureScale) throws NullPointerException{
			if(newTemperatureScale == null){
				throw new NullPointerException();
			}
			if(newTemperatureScale != this.temperatureScale){
				this.minTemperature = Scale.changeScale(getTemperatureScale(),newTemperatureScale, getMinTemperature());
				this.maxTemperature = Scale.changeScale(getTemperatureScale(),newTemperatureScale, getMaxTemperature());
				this.temperature = Scale.changeScale(getTemperatureScale(),newTemperatureScale, getTemperature());
				this.temperatureScale = newTemperatureScale;
			}
	}
	private void calculateColdDamage(){
		Scale oldScale = getTemperatureScale();
		changeScale(Scale.CELCIUS);
		if(temperature <-5){
			this.coldDamage = (temperature + 5)/10;
		}
		else
			this.coldDamage = 0;
		changeScale(oldScale);
	}
	/**
	 * 
	 * @return
	 */
	public static int getHeatLimit() {
		return heatLimit;
	}
	/**
	 * 
	 * @return
	 */
	public static int getHeatRate() {
		return heatRate;
	}
	/**
	 * 
	 * @param newHeatLimit
	 */
	public static void setHeatLimit(int newHeatLimit) {
		heatLimit = newHeatLimit;
	}
	/**
	 * 
	 * @param newHeatRate
	 */
	public static void setHeatRate(int newHeatRate) {
		heatRate = newHeatRate;
	}
	/**
	 * 
	 */
	private void calculateHeatDamage(){
		Scale oldScale = getTemperatureScale();
		changeScale(Scale.CELCIUS);
		if(getTemperature()>35)
			 heatDamage = (getTemperature() - getHeatLimit())/getHeatRate();
		else
			heatDamage = 0;
		changeScale(oldScale);
	}
	/**
	 * 
	 * @return
	 */
	@Basic
	public int getColdDamage(){
		return coldDamage;
	}
	/**
	 * 
	 * @return
	 */
	@Basic
	public int getHeatDamage(){
		return heatDamage;
	}
	/**
	 * 
	 * @return
	 */
	@Basic
	public BigDecimal getHumidity(){
		return humidity;
	}
	/**
	 * 
	 * @param humidity
	 * @return whether or not the humidity is correct
	 * 			| result == (humidity>=0 && humidity<=100)
	 */
	public boolean canHaveHumidity(float humidity){
		return humidity>=0 && humidity<=100;
	}
	/**
	 * @pre The humidity must be situated in the valid range
	 * 		| canHaveHumidity(humidity)
	 * @param humidity2 to set for this square.
	 * @post the humidity is set to the given value.
	 * 		| new.getHumidity() == humidity
	 */
	public void setHumidity(BigDecimal humidity){
		this.humidity = humidity;
	}
	/**
	 * This method calculates the rustDamage due to humidity
	 * @post if the humidity is larger than 30 percent, then the rust damage will be calculated
	 * 			|if(this.getHumidity()>30f)
	 * 			| new.getRustDamage() == (int) ((this.getHumidity()-30)/7)
	 * @post else the humidity is set to 0.
	 * 			new.getRustDamage() == 0.
	 */
	private void calculateRustDamage(){
		if(getHumidity().floatValue() > 30){
			this.rustDamage = ((getHumidity().subtract(new BigDecimal(30))).divide(new BigDecimal(7))).intValue();
		}
		else{
			this.rustDamage = 0;
		}
	}
	@Basic
	public int getRustDamage(){
		return rustDamage;
	}
	@Basic
	public int getInhability(){
		return inhability;
	}
	/**
	 * @post the inhability of the square is calculated.
	 * 		| new.getInhability() == (-(Math.sqrt(Math.pow(getHeatDamage(),3)))/(101-getHumidity())-Math.sqrt(getColdDamage()))
	 */
	private void calculateInhability(){
		this.inhability =  (int) (-(Math.sqrt(Math.pow(getHeatDamage(),3)))/(101-getHumidity().floatValue())-Math.sqrt(getColdDamage()));
		
	}
	@Basic
	public boolean getSlippery(){
		return this.slippery;
	}
	@Basic
	public boolean getSlipperyMaterial(){
		return this.slipperyMaterial;
	}
	public void setSlipperyMaterial(boolean isSlipperyMaterial){
		this.slipperyMaterial = isSlipperyMaterial;
	}
	/**
	 * @post 	the slipperiness of the square is calculated
	 * 			|if(this.getHumidity() == 100f && this.getTemperature()> 0)
	 * 			|	new.getSlippery()==true
	 * 			|else if(this.getTemperature() <=0 && this.getHumidity()>10f)
	 * 			|	new.getSlippery() == true
	 * 			|else if (getSlipperyMaterial())
	 * 			|	new.getSlippery() == true
	 * 			|else
	 * 			|	new.getSlippery()==false
	 */
	private void calculateSlippery(){
		this.setSlippery(getSlipperyMaterial());
		if(this.getHumidity().equals(new BigDecimal(100)) && this.getTemperature()> 0)
			this.setSlippery(true);
		else if(this.getTemperature() <=0 && this.getHumidity().floatValue()>10f){
			this.setSlippery(true);
		}
			
	}
	private void setSlippery(boolean isSlippery){
		this.slippery = isSlippery;
	}

	/**
	 * 
	 * @param index	
	 * 			the direction of the border to add
	 * @post	If the index is smaller than or equal to the length of the array and larger than 0, the border in the direction of the index is set.
	 * 			|if(canHaveAsDirection(index))
	 * 			| new.getBorder(index-1)==true 
	 * @post	If the index is larger than the length of the array or smaller than 1, nothing changes.
	 * 			| 
	 */
	public void addBorder(int index){
		if(canHaveAsDirection(index))
			borders[index-1]=true;
	}
	/**
	 * 
	 * @param index	
	 * 			the direction of the border to remove
	 * @post	If the index is smaller than or equal to the length of the array and larger than 0, the border in the direction of the index is set to false.
	 * 			|if(canHaveAsDirection(index))
	 * 			| new.getBorder(index-1)==false 
	 * @post	If the index is larger than the length of the array or smaller than 1, nothing changes.
	 * 			| 
	 */
	public void removeBorder(int index) {
		if(canHaveAsDirection(index))
			borders[index-1] = false;
	}
	/**
	 * 
	 * @param index	
	 * 			the direction of the border to access
	 * @return	If the index is smaller than or equal to the length of the array and larger than 0, the border in the direction of the index is returned.
	 * 			|if(canHaveAsDirection(index))
	 * 			| result == getBorder(index-1)
	 * @return	If the index is larger than the length of the array or smaller than 1, it returns false.
	 * 			| result == false
	 */
	public boolean getBorder(int index) {
		if(canHaveAsDirection(index))
			return borders[index-1];
		else
			return false;
	}
	/**
	 * 
	 * @param index
	 * 			the direction of the border
	 * @return whether or not the given index is a valid one
	 * 			|result == the direction of the border
	 */
	public boolean canHaveAsDirection(int index){
		return index<=6 && index>0;
	}
	/**
	 * 
	 * @param index of the direction with which you want to merge
	 * @param otherSquare is the square with whom you merge
	 * @post 	...
	 * 			| new.getTemperature() = calculateTemperature(this.getTemperature(), this.getHumidity(), newHumidity)
	 * @post ...
	 * 			| new.getHumidity() = calculateHumidity(this.getHumidity(), otherSquare.getHumidity())
	 */
	public void mergeWith(int index,  Square otherSquare){
		this.removeBorder(index);
		otherSquare.removeBorder(index);
		BigDecimal newHumidity = calculateHumidity(this.getHumidity(), otherSquare.getHumidity());
		int newTemperature = calculateTemperature(this.getTemperature(), this.getHumidity(), newHumidity);
		int otherNewTemperature = calculateTemperature(otherSquare.getTemperature(), otherSquare.getHumidity(), newHumidity);
		otherSquare.setTemperature(otherNewTemperature);
		this.setHumidity(newHumidity);
		otherSquare.setHumidity(newHumidity);
		this.setTemperature(newTemperature);
	}
	/**
	 * 
	 * @param humidity
	 * 			the given humidity for this square
	 * @param otherHumidity
	 * 			the given humidity of the other square
	 * @return the average of the 2 humidities.
	 * 			| Math.round((humidity + otherHumidity)/2*100)/100
	 */
	private BigDecimal calculateHumidity(BigDecimal humidity, BigDecimal otherHumidity){
		return (humidity.add(otherHumidity)).divide(new BigDecimal(2));
		
	}
	/**
	 * 
	 * @param temperature: the given temperature of the square.
	 * @param humidity the given humidity of the square.
	 * @param newHumidity: the new humidity calculated for both squares
	 * @return ((2- 2* getHeatConstant()*humidity)/2*newHumidity)
	 */
	private int calculateTemperature(int temperature, BigDecimal humidity, BigDecimal newHumidity){
		float humidityConstant = 2- 2* getHeatConstant();
		return (int) ((humidityConstant*humidity.floatValue())/(2*newHumidity.floatValue()));
	}
	/**
	 * 
	 * @return the heatConstant of this square
	 */
	@Basic
	public float getHeatConstant(){
		return heatConstant;
	}
	/**
	 * 
	 * @param heatConstant
	 * 			that is the heatConstant given to this square
	 * @return whether the constant is a valid one
	 * 			| result == getHeatConstant()> 0.1 && getHeatConstant() <0.4;
	 */
	@Raw
	public boolean canHaveHeatConstant(float heatConstant){
		return heatConstant>=0.1f && heatConstant <=0.4f;
	}
	/**
	 * Variable registering the state of this square (terminated or not).
	 */
	private boolean isTerminated;
	/**
	 * Method to set the state of this square to terminated.
	 */
	public void Terminate() {
		this.isTerminated = true;
	}
	
	/**
	 * 
	 * @return whether or not this square is terminated.
	 */
	public boolean isTerminated() {
		return this.isTerminated;
	}
}
